package com.miage.ldapsec.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Service;

import com.miage.ldapsec.obj.*;

@Service
public class LDAPServiceImpl implements LDAPService {

    private DirContext context;

    
    //Constructor

    public LDAPServiceImpl() throws IOException {
        
        Resource resource = new ClassPathResource("WEB-INF/config/ldap.properties");
        Properties props = PropertiesLoaderUtils.loadProperties(resource);
        
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, (String) props.get("ldap.hostname"));
        env.put(Context.SECURITY_PRINCIPAL, (String) props.get("ldap.login"));
        env.put(Context.SECURITY_CREDENTIALS, (String) props.get("ldap.passwd"));

        try {
            this.context = new InitialDirContext(env);
        } catch (NamingException e) {
            //e.printStackTrace();
        }
    }


    //Context

    public DirContext getContext() {
        return this.context;
    }

    public boolean close() {
        try {
			this.context.close();
		} catch (NamingException e) {
			return false;
		}
        return true;
    }

    
    //Users

    private Attributes prepareUserAttrs(User user) {

        Attributes attrs = new BasicAttributes(true);
        Attribute oc = new BasicAttribute("objectClass");
        oc.add("inetOrgPerson");
        oc.add("organizationalPerson");
        oc.add("person");
        oc.add("top");

        attrs.put(oc);
        attrs.put("uid", user.getLogin());
        attrs.put("cn", user.getCommonName());
        attrs.put("sn", user.getSurname());
        attrs.put("givenName", user.getGivenName());
        if (user.getPassword() != null) {
            attrs.put("userPassword", user.getPassword());
        }

        return attrs;
    }

    public boolean createUser(User user) {

        Attributes attrs = prepareUserAttrs(user); 

        try {
            this.context.createSubcontext(user.getDn(), attrs);
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
        return true;
    }

    public boolean modifyUser(User user) {

        Attributes attrs = prepareUserAttrs(user);

        try {
            this.context.modifyAttributes(user.getDn(), DirContext.REPLACE_ATTRIBUTE, attrs);
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
        return true;
    }

    public boolean removeUser(User user) {
        try {
            for (Group group : getAllGroups()) {
                removeUserFromGroup(group, user);
            }
            this.context.unbind(user.getDn());
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        	return false;
        }
        return true;
    }

    private User getUserWithDn(String dn) {
        
        String uid = null;
        String sn = null;
        String gn = null;
        String pwd = null;

        String[] attrSearch = {"uid", "sn", "givenName", "userPassword"};
        Attributes attrs;
		
        try {
			attrs = this.context.getAttributes(dn, attrSearch);
			
	        NamingEnumeration<? extends Attribute> ae = attrs.getAll();
	        String attribut;
	        String value;
	
	        while (ae.hasMore()) {
	
	            Attribute attr = (Attribute) ae.next();
	            attribut = attr.getID();
	            value = attr.get().toString();
	
	            if (attribut.equals("uid")) {
	                uid = value;
	            } else if (attribut.equals("sn")) {
	                sn = value;
	            } else if (attribut.equals("givenName")) {
	            	gn = value;
	            } else if (attribut.equals("userPassword")) {
	            	pwd = value;
	            }
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}

        return new User(uid, sn, gn, pwd);
    }

    private ArrayList<User> getUsersGroup(Group group) {

        ArrayList<User> arrUser = new ArrayList<User>();

        String[] attrSearch = {"member"};
        Attributes attrs;
		
        try {
			attrs = this.context.getAttributes(group.getDn(), attrSearch);
		
	        NamingEnumeration<? extends Attribute> ae = attrs.getAll();
	
	        while (ae.hasMore()) {
	            Attribute attr = (Attribute) ae.next();
	            NamingEnumeration<?> member = attr.getAll();
	
	            while (member.hasMore()) {
	                arrUser.add(getUserWithDn(member.nextElement().toString()));
	            }
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}

        return arrUser;
    }

    private ArrayList<User> getUserWithFilter(String filter) {

        ArrayList<User> arrUser = new ArrayList<User>();

        SearchControls sc = new SearchControls();
        String[] attributeFilter = {"uid", "sn", "givenName"};
        sc.setReturningAttributes(attributeFilter);
        sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

        NamingEnumeration<SearchResult> answer;
		try {
			answer = this.context.search("ou=people,o=root", filter, sc);
		
	        while (answer.hasMore()) {
	            SearchResult sr = (SearchResult) answer.next();
	
	            String uid = sr.getAttributes().get("uid").get().toString();
	            String sn = sr.getAttributes().get("sn").get().toString();
	            String gn = sr.getAttributes().get("givenName").get().toString();
	
	            arrUser.add(new User(uid, sn, gn, ""));
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}

        return arrUser;
    }

    public User getUser(String uid){
        String filter = "(uid=" + uid + ")";

        ArrayList<User> userList = getUserWithFilter(filter);
        
        if(!userList.isEmpty()){
            return userList.get(0);
        }
        
        return null;
    }

    public ArrayList<User> getAllUsers(){
        String filter = "(uid=*)";
        return getUserWithFilter(filter);
    }

    public ArrayList<User> getOtherUsers(ArrayList<User> arrUser) {
    	
    	ArrayList<User> allUsers = getAllUsers();

        for (int i = 0 ; i < allUsers.size() ; i++) {
            for (int j = 0 ; j < arrUser.size() ; j++) {
                if(allUsers.get(i).getLogin().equals(arrUser.get(j).getLogin())) {
                    allUsers.remove(i);
                    --i;
                    break;
                }
            }
        }
        return allUsers;
    }
    
    public ArrayList<User> searchUsers(String search) {
        String filter = "(|(uid=" + search + "*)(cn=" + search + "*)(sn=" + search + "*)(gn=" + search + "*))";
        return getUserWithFilter(filter);
    }

    public int nbUsers() {
        return getAllUsers().size();
    }
    

    //Groups

    public boolean addUserToGroup(Group group, User user) {

        Attributes attrs = new BasicAttributes(true);
        attrs.put("member", user.getDn());

        try {
            this.context.modifyAttributes(group.getDn(), DirContext.ADD_ATTRIBUTE, attrs);
            group.getArrUser().add(user);
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        	return false;
        }
        return true;
    }

    public void removeUserFromGroup(Group group, User user) {

        Attributes attrs = new BasicAttributes(true);
        attrs.put("member", user.getDn());

        try {
            this.context.modifyAttributes(group.getDn(), DirContext.REMOVE_ATTRIBUTE, attrs);
            group.getArrUser().remove(user);
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private ArrayList<Group> getAllGroups() {
        ArrayList<Group> arrGroup = new ArrayList<Group>();
        arrGroup.addAll(getAllRoleGroups());
        arrGroup.addAll(getAllUserGroups());
        return arrGroup;
    }


    //UserGroups

    private Attributes prepareUserGroupAttrs(UserGroup group) {

        Attributes attrs = new BasicAttributes(true);
        Attribute oc = new BasicAttribute("objectClass");
        oc.add("group");
        oc.add("top");

        attrs.put(oc);
        attrs.put("cn", group.getCommonName());

        return attrs;
    }

    public boolean createUserGroup(UserGroup group) {

        Attributes attrs = prepareUserGroupAttrs(group);

        try {
            this.context.createSubcontext(group.getDn(), attrs);
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        	return false;
        }
        return true;
    }

    public boolean modifyUserGroup(UserGroup oldGroup, UserGroup newGroup) {

        try {
            createUserGroup(newGroup);
            createMember(oldGroup, newGroup);
            removeUserGroup(oldGroup);
        } catch (Exception ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        	return false;
        }
        return true;
    }

    private void createMember(UserGroup oldGroup, UserGroup newGroup) {
        ArrayList<User> member = oldGroup.getArrUser();
        newGroup.getArrUser().clear();
        for(User user : member) {
            addUserToGroup(newGroup, user);
        }
    }

    public boolean removeUserGroup(UserGroup group) {
        try {
            this.context.unbind(group.getDn());
        } catch (NamingException ex) {
            //Logger.getLogger(LDAPService.class.getName()).log(Level.SEVERE, null, ex);
        	return false;
        }
        return true;
    }

    private ArrayList<UserGroup> getUserGroupsWithFilter(String filter) {

        ArrayList<UserGroup> arrGroup = new ArrayList<UserGroup>();

        SearchControls sc = new SearchControls();
        String[] attributeFilter = {"cn"};
        sc.setReturningAttributes(attributeFilter);
        sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

        NamingEnumeration<SearchResult> answer;
		try {
			answer = this.context.search("ou=groups,o=root", filter, sc);

	        while (answer.hasMore()) {
	            SearchResult sr = (SearchResult) answer.next();
	            UserGroup userGroup = new UserGroup(sr.getAttributes().get("cn").get().toString());
	            userGroup.setArrUser(getUsersGroup(userGroup));
	            arrGroup.add(userGroup);
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}

        return arrGroup;
    }

    public UserGroup getUserGroup(String cn) {
        String filter = "(cn=" + cn + ")";
        return getUserGroupsWithFilter(filter).get(0);
    }

    public ArrayList<UserGroup> getAllUserGroups() {
        String filter = "(cn=*)";
        return getUserGroupsWithFilter(filter);
    }

    public int nbUserGroups() {
        return getAllUserGroups().size();
    }

    
    //RoleGroups
    
    private ArrayList<RoleGroup> getRoleGroupsWithFilter(String filter) {

        ArrayList<RoleGroup> arrGroup = new ArrayList<RoleGroup>();

        SearchControls sc = new SearchControls();
        String[] attributeFilter = {"cn"};
        sc.setReturningAttributes(attributeFilter);
        sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

        NamingEnumeration<SearchResult> answer;
		try {
			answer = this.context.search("ou=roles,o=root", filter, sc);

	        while (answer.hasMore()) {
	            SearchResult sr = (SearchResult) answer.next();
	            RoleGroup roleGroup = new RoleGroup(sr.getAttributes().get("cn").get().toString());
	            roleGroup.setArrUser(getUsersGroup(roleGroup));
	            arrGroup.add(roleGroup);
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}

        return arrGroup;
    }

    public RoleGroup getRoleGroup(String cn) {
        String filter = "(cn=" + cn + ")";
        return getRoleGroupsWithFilter(filter).get(0);
    }

    public ArrayList<RoleGroup> getAllRoleGroups() {
        String filter = "(cn=*)";
        return getRoleGroupsWithFilter(filter);
    }
    
    public void removeOtherRoles(RoleGroup roleGroup, User user) {
    	for (RoleGroup role : getAllRoleGroups()) {
    		if(!roleGroup.getCommonName().equals(role.getCommonName())) {
    			removeUserFromGroup(role, user);
    		}
        }
    }


    //Tree

    public String getTree(String dn) {
        StringBuilder str = new StringBuilder("> " + dn);
        str.append(displayTree(dn));
        return str.toString();
    }

    private String displayTree(String dn) {
        StringBuilder str = new StringBuilder();
        NamingEnumeration<Binding> bindings;
		
        try {
			bindings = this.context.listBindings(dn);

	        while (bindings.hasMore()) {
	            Binding bd = (Binding) bindings.next();
	            str.append("\n> ").append(bd.getName());
	            str.append(displayTree(bd.getName() + "," + dn));
	        }
		} catch (NamingException e) {
			//e.printStackTrace();
		}
        return str.toString();
    }

}